/**
 * @ignore
 * setup event/dom api module
 * @author yiminghe@gmail.com
 */

var BaseEvent = require('event/base');
var DomEventUtils = require('./utils');
var Dom = require('dom');
var Special = require('./special');
var DomEventObservable = require('./observable');
var DomEventObject = require('./object');
var util = require('util');
var BaseUtils = BaseEvent.Utils;

function fixType(cfg, type) {
    var s = Special[type] || {},
        typeFix;

    // in case overwritten by typeFix in special events
    // (mouseenter/leave,focusin/out)
    if (!cfg.originalType && (typeFix = s.typeFix)) {
        // when on mouseenter, it's actually on mouseover,
        // and observers is saved with mouseover!
        cfg.originalType = type;
        type = typeFix;
    }

    return type;
}

function addInternal(currentTarget, type, cfg) {
    var domEventObservablesHolder, domEventObservable,
        domEventObservables, handle;

    cfg = util.merge(cfg);
    type = fixType(cfg, type);

    // 获取事件描述
    domEventObservablesHolder = DomEventObservable.getDomEventObservablesHolder(currentTarget, 1);

    if (!(handle = domEventObservablesHolder.handle)) {
        handle = domEventObservablesHolder.handle = function (event) {
            // 是经过 fire 手动调用而浏览器同步触发导致的，就不要再次触发了，
            // 已经在 fire 中 bubble 过一次了
            // in case after page has unloaded
            var type = event.type,
                domEventObservable,
                currentTarget = handle.currentTarget;
            if (DomEventObservable.triggeredEvent === type ||
                typeof KISSY === 'undefined') {
                return undefined;
            }
            domEventObservable = DomEventObservable.getDomEventObservable(currentTarget, type);
            if (domEventObservable) {
                event.currentTarget = currentTarget;
                event = new DomEventObject(event);
                return domEventObservable.notify(event);
            }
            return undefined;
        };
        handle.currentTarget = currentTarget;
    }

    if (!(domEventObservables = domEventObservablesHolder.observables)) {
        domEventObservables = domEventObservablesHolder.observables = {};
    }

    //事件 listeners , similar to eventListeners in Dom3 Events
    domEventObservable = domEventObservables[type];

    if (!domEventObservable) {
        domEventObservable = domEventObservables[type] = new DomEventObservable({
            type: type,
            currentTarget: currentTarget
        });

        domEventObservable.setup();
    }

    domEventObservable.on(cfg);

    currentTarget = null;
}

function removeInternal(currentTarget, type, cfg) {
    // copy
    cfg = util.merge(cfg);

    var customEvent;

    type = fixType(cfg, type);

    var domEventObservablesHolder = DomEventObservable.getDomEventObservablesHolder(currentTarget),
        domEventObservables = (domEventObservablesHolder || {}).observables;

    if (!domEventObservablesHolder || !domEventObservables) {
        return;
    }

    // remove all types of event
    if (!type) {
        for (type in domEventObservables) {
            domEventObservables[type].detach(cfg);
        }
        return;
    }

    customEvent = domEventObservables[type];

    if (customEvent) {
        customEvent.detach(cfg);
    }
}

/**
 * Dom Event Management
 * @private
 * @class KISSY.Event.DomEvent
 */
var DomEvent = {
    /**
     * Adds an event listener.similar to addEventListener in Dom3 Events
     * @param targets KISSY selector
     * @param type {String} The type of event to append.
     * use space to separate multiple event types.
     * @param fn {Function|Object} The event listener or event description object.
     * @param {Function} fn.fn The event listener
     * @param {Function} fn.context The context (this reference) in which the handler function is executed.
     * @param {String|Function} fn.filter filter selector string or function to find right element
     * @param {Boolean} fn.once whether fn will be removed once after it is executed.
     * @param {Object} [context] The context (this reference) in which the handler function is executed.
     */
    on: function (targets, type, fn, context) {
        // data : 附加在回调后面的数据，delegate 检查使用
        // remove 时 data 相等(指向同一对象或者定义了 equals 比较函数)
        targets = Dom.query(targets);

        BaseUtils.batchForType(function (targets, type, fn, context) {
            var cfg = BaseUtils.normalizeParam(type, fn, context), i, t;
            type = cfg.type;
            for (i = targets.length - 1; i >= 0; i--) {
                t = targets[i];
                addInternal(t, type, cfg);
            }
        }, 1, targets, type, fn, context);

        return targets;
    },

    /**
     * Detach an event or set of events from an element. similar to removeEventListener in Dom3 Events
     * @param targets KISSY selector
     * @param {String|Boolean} [type] The type of event to remove.
     * use space to separate multiple event types.
     * or
     * whether to remove all events from descendants nodes.
     * @param [fn] {Function|Object} The event listener or event description object.
     * @param {Function} fn.fn The event listener
     * @param {Function} [fn.context] The context (this reference) in which the handler function is executed.
     * @param {String|Function} [fn.filter] filter selector string or function to find right element
     * @param {Boolean} [fn.once] whether fn will be removed once after it is executed.
     * @param {Object} [context] The context (this reference) in which the handler function is executed.
     */
    detach: function (targets, type, fn, context) {
        targets = Dom.query(targets);

        BaseUtils.batchForType(function (targets, singleType, fn, context) {
            var cfg = BaseUtils.normalizeParam(singleType, fn, context),
                i, j, elChildren, t;

            singleType = cfg.type;

            for (i = targets.length - 1; i >= 0; i--) {
                t = targets[i];
                removeInternal(t, singleType, cfg);
                // deep remove
                if (cfg.deep && t.getElementsByTagName) {
                    elChildren = t.getElementsByTagName('*');
                    for (j = elChildren.length - 1; j >= 0; j--) {
                        removeInternal(elChildren[j], singleType, cfg);
                    }
                }
            }
        }, 1, targets, type, fn, context);

        return targets;
    },

    /**
     * Delegate event.
     * @param targets KISSY selector
     * @param {String|Function} filter filter selector string or function to find right element
     * @param {String} [eventType] The type of event to delegate.
     * use space to separate multiple event types.
     * @param {Function} [fn] The event listener.
     * @param {Object} [context] The context (this reference) in which the handler function is executed.
     *
     */
    delegate: function (targets, eventType, filter, fn, context) {
        return DomEvent.on(targets, eventType, {
            fn: fn,
            context: context,
            filter: filter
        });
    },
    /**
     * undelegate event.
     * @param targets KISSY selector
     * @param {String} [eventType] The type of event to undelegate.
     * use space to separate multiple event types.
     * @param {String|Function} [filter] filter selector string or function to find right element
     * @param {Function} [fn] The event listener.
     * @param {Object} [context] The context (this reference) in which the handler function is executed.
     *
     */
    undelegate: function (targets, eventType, filter, fn, context) {
        return DomEvent.detach(targets, eventType, {
            fn: fn,
            context: context,
            filter: filter
        });
    },

    /**
     * fire event,simulate bubble in browser. similar to dispatchEvent in Dom3 Events
     * @param targets html nodes
     *
     * @param {String} eventType event type
     * @param [eventData] additional event data
     * @param {Boolean} [onlyHandlers] for internal usage
     * @return {*} return false if one of custom event 's observers (include bubbled) else
     * return last value of custom event 's observers (include bubbled) 's return value.
     */
    fire: function (targets, eventType, eventData, onlyHandlers) {
        var ret;

        // fire(eventObject) === dispatchEvent(eventObject)
        if (eventType.isEventObject) {
            eventData = eventType;
            eventType = eventType.type;
        }

        // custom event firing moved to target.js
        eventData = eventData || {};

        /**
         * identify event as fired manually
         * @ignore
         */
        eventData.synthetic = 1;

        BaseUtils.splitAndRun(eventType, function (eventType) {
            var r, i, target, domEventObservable;

            BaseUtils.fillGroupsForEvent(eventType, eventData);

            // mouseenter
            eventType = eventData.type;
            var s = Special[eventType];

            var originalType = eventType;

            // where observers lie
            // mouseenter observer lies on mouseover
            if (s && s.typeFix) {
                // mousemove
                originalType = s.typeFix;
            }

            targets = Dom.query(targets);

            for (i = targets.length - 1; i >= 0; i--) {
                target = targets[i];
                domEventObservable = DomEventObservable.getDomEventObservable(target, originalType);
                // bubbling
                // html dom event defaults to bubble
                if (!onlyHandlers && !domEventObservable) {
                    domEventObservable = new DomEventObservable({
                        type: originalType,
                        currentTarget: target
                    });
                }
                if (domEventObservable) {
                    r = domEventObservable.fire(eventData, onlyHandlers);
                    if (ret !== false && r !== undefined) {
                        ret = r;
                    }
                }
            }
        });

        return ret;
    },

    /**
     * same with fire but:
     * - does not cause default behavior to occur.
     * - does not bubble up the Dom hierarchy.
     * @param targets html nodes
     *
     * @param {String} eventType event type
     * @param [eventData] additional event data
     * @return {*} return false if one of custom event 's observers (include bubbled) else
     * return last value of custom event 's observers (include bubbled) 's return value.
     */
    fireHandler: function (targets, eventType, eventData) {
        return DomEvent.fire(targets, eventType, eventData, 1);
    },

    /**
     * copy event from src to dest
     *
     * @param {HTMLElement} src srcElement
     * @param {HTMLElement} dest destElement
     * @private
     */
    clone: function (src, dest) {
        var domEventObservablesHolder, domEventObservables;
        if (!(domEventObservablesHolder = DomEventObservable.getDomEventObservablesHolder(src))) {
            return;
        }
        var srcData = DomEventUtils.data(src);
        if (srcData && srcData === DomEventUtils.data(dest)) {
            // remove event data (but without dom attached listener)
            // which is copied from above Dom.data
            DomEventUtils.removeData(dest);
        }
        domEventObservables = domEventObservablesHolder.observables;
        util.each(domEventObservables, function (customEvent, type) {
            util.each(customEvent.observers, function (observer) {
                // context undefined
                // 不能 this 写死在 handlers 中
                // 否则不能保证 clone 时的 this
                addInternal(dest, type, observer.config);
            });
        });
    },

    getEventListeners: function (target, type) {
        var observables = (DomEventObservable.getDomEventObservablesHolder(target) || {
            observables: {}
        }).observables;
        return type ? observables[type] : observables;
    }
};

module.exports = DomEvent;

/*
 2012-02-12 yiminghe@gmail.com note:
 - 普通 remove() 不管 filter 都会查，如果 fn context 相等就移除
 - undelegate() filter 为 ''，那么去除所有委托绑定的 handler
 */
